package org.sergiy.metawidget.rules;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.drools.FactHandle;
import org.drools.RuntimeDroolsException;
import org.drools.RuleBase;
import org.drools.RuleBaseFactory;
import org.drools.WorkingMemory;
import org.drools.compiler.DroolsError;
import org.drools.compiler.DroolsParserException;
import org.drools.compiler.PackageBuilder;
import org.drools.compiler.PackageBuilderConfiguration;
import org.drools.compiler.DescrBuildError;
import org.drools.rule.Package;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;


/**
 * <UTILITY>
 * 
 * Factory for managing compilation of Drools packages from different sources
 * 
 * @author Sergiy Litsenko
 *
 */
public class PackageFactory {
    private static final Log log = LogFactory.getLog (PackageFactory.class);
    private static final PackageBuilderConfiguration conf = new PackageBuilderConfiguration();

    /**
     * Don't need to instantiate instances for factory
     */
    private PackageFactory () {
	}
    
    /**
     * Log Drools package builder errors using Seam log service
     * 
     * @param builder the Drools package builder to check for errors
     * @param drlName the full name of DRL stream (for logging purposes)
     * @param errorBuffer the buffer to store all errors
     * 
     */
    protected static void logPackageBuilderErrors (PackageBuilder builder, String drlFile, StringBuffer errorBuffer) {
    	// check input
    	if ((builder == null) || (errorBuffer == null) || !builder.hasErrors()) {
    		return; 
    	}
    	errorBuffer.append(String.format("The %1$s source file contains compilation errors:\n", drlFile));
       	// log all errors
        for (DroolsError error: builder.getErrors().getErrors()) {
        	if (error instanceof DescrBuildError) {
        		DescrBuildError ruleError = (DescrBuildError) error;
        		errorBuffer.append(String.format("\t Rule file: %1$s, line: %2$d: %3$s.\n", drlFile, ruleError.getLine(), ruleError.getMessage()));                     
            } else {
            	errorBuffer.append( String.format("\t Rule file: %1$s: %2$s (%3$s).\n", drlFile, error.getMessage(), error.toString()));                     
            }
        }
    }

    /**
     * Opens input stream for Drools source (DRL/RFM)  
     * @param src the Drools source (DRL/RFM). Could be instance of String, File or URL
     * @return input stream or throws Exception
     */
    protected static InputStream getInputStreamFromSource (Object src) throws Exception {
    	InputStream inputStream = null;
    	// get context class loader
     	ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
		// compile DRL resource from local file name
		if (src instanceof String) {
			// first try - use context class loader
		      	if (classLoader != null) {
		      		inputStream = classLoader.getResourceAsStream((String)src);
		      	}
		      	// second try - use class 
		      	if (inputStream == null) {
		      		inputStream = PackageFactory.class.getResourceAsStream((String)src);
		      	}
		      	// third try - use class loader
		      	if (inputStream == null) {
		      		inputStream  = PackageFactory.class.getClassLoader().getResourceAsStream((String)src);
		      	}
		      	
		      	if (inputStream == null) {
		      		inputStream  = PackageFactory.class.getResourceAsStream((String)src);
		      	}
		      	
		      	if (inputStream == null) {
		      		inputStream  = new FileInputStream(new File((String)src));
		      	}
		} else
		// compile DRL resource from URL
		if (src instanceof URL) {
			inputStream  = ((URL)src).openStream();
		} else
		
		// compile DRL resource from File
		if (src instanceof File) {
			inputStream  = new FileInputStream((File)src);
		}
		
		// check that input stream defined 
		if (inputStream == null) {
			throw new FileNotFoundException ((src != null)? src.toString() : null);
		}
		// return 
		return inputStream;
	}
    
    /**
     * Compiles drl source stream using package builder
     *  
     * @param builder the builder to compile package
     * @param drlStream the DRL source stream
     * @param drlName the full name of DRL stream (for logging purposes)
     */
    public static void compilePackage  (PackageBuilder builder, InputStream drlStream, String drlName) {
    	String errInfo = null;
		// log debug info
		if (log.isDebugEnabled()) {
			log.debug(String.format("[Drools package compiler]: Compiling the %1$s source file...", drlName));
		}
    	
    	// check builder
    	if (builder == null) {
    		errInfo = String.format("[Drools package compiler]: The package builder shall be defined");
        	log.error(errInfo);
        	throw new IllegalArgumentException (errInfo);
    	}
    	// check input stream
    	if (drlStream == null) {
        	errInfo = String.format("[Drools package compiler]: The %1$s source file is not found.", drlName); 
        	log.error(errInfo);
        	throw new IllegalArgumentException (errInfo);
    	}	
        try {
			// read the DRL source file using java.io.reader
			builder.addPackageFromDrl(new InputStreamReader(drlStream));
	    	// errors reported
	    	if (builder.hasErrors()) {
	    		// Log Drools package builder errors using Seam log service
	    		StringBuffer errorBuffer = new StringBuffer ();
	    		logPackageBuilderErrors (builder, drlName, errorBuffer);
	    		log.error (errorBuffer.toString());
	    		throw new RuntimeDroolsException (errorBuffer.toString());
	    	} else {
	        	// log no errors
	        	if (log.isDebugEnabled()) {
	        		log.debug(String.format("[Drools package compiler]: No errors found by '%1$s' dialect of DROOLS package builder for the '%2$s' source file.", 
	        				builder.getDefaultDialect(), drlName));
	        	}
	    	}
		} catch (Exception e) {
			if (e instanceof RuntimeDroolsException) {
				// log already contains the information about exception - just re-throw it
				throw (RuntimeDroolsException)e;
			} else {
				errInfo = String.format("[Drools package compiler]: error compiling the source file: %1$s", drlName); 
				log.error(errInfo);
				throw new RuntimeDroolsException (errInfo, e);
			}	
		}   
    }

    /**
     * Compiles a package from one or more source files (DRL)
     * 
     * @param drlFiles source files. Supported file names as strings and as URLs
     * @return compiled package or throws Runtime Exception
     */
    public static Package compilePackage (Object ... drlFiles) {
    	return compilePackage (null, drlFiles);
    }
    
    /**
     * Compiles a package from one or more source files - DRL and rule flow
     * 
     * @param  ruleFlow the rule flow for drl files or null
     * @param drlFiles source files. Supported file names as strings and as URLs
     * @return compiled package or throws Runtime Exception
     */
    public static Package compilePackage (Object ruleFlow, Object ... drlFiles) {
    	// check input
    	if ((drlFiles == null) || (drlFiles.length == 0)) {
    		String errorInfo = "Drl files shall be defined";
    		log.error (errorInfo);
    		throw new IllegalArgumentException (errorInfo);
    	}
    	// create Drools package builder
    	PackageBuilder builder = new PackageBuilder (conf);
    	InputStream inputStream = null;
    	String fileName = null;
		// log debug info
		if (log.isDebugEnabled()) {
			log.debug(String.format("[Drools package compiler]: Compiling the '%1$s' source files...", drlFiles));
		}
   	
    	try {
    		for (Object drlFile: drlFiles) {
    			// log debug info
    			if (log.isDebugEnabled()) {
    				log.debug(String.format("[Drools package compiler]: Adding the '%1$s' source file...", drlFile));
    			}
    			// open input stream 
			    inputStream  = getInputStreamFromSource (drlFile);
    			
    			// compile DRL resource from local file name
    			if (drlFile instanceof String) {
    				fileName = drlFile.toString();
    			} else
    			// compile DRL resource from URL
    			if (drlFile instanceof URL) {
    				fileName = ((URL)drlFile).toExternalForm();
    			} else
    			// compile DRL resource from File
    			if (drlFile instanceof File) {
    				fileName = ((File)drlFile).toString();
    			}
    			// compile the resource
    			compilePackage  (builder, inputStream, fileName);
    			
        		// close input stream
        		IOUtils.closeQuietly (inputStream);
        		inputStream = null;
    		}
    		//
    		// add rule flow if defined
    		//
    		if (ruleFlow != null) {
    			// log debug info
    			if (log.isDebugEnabled()) {
    				log.debug(String.format("[Drools package compiler]: Adding the '%1$s' Rule Flow source file...", ruleFlow));
    			}
    			// open input stream 
			    inputStream  = getInputStreamFromSource (ruleFlow);
			    // add rule flow
			    builder.addRuleFlow(new InputStreamReader(inputStream));
        		// close input stream
        		IOUtils.closeQuietly (inputStream);
        		inputStream = null;
    		}
    		
			// log debug info
			if (log.isDebugEnabled()) {
				log.debug(String.format("[Drools package compiler]: Creating the binary package..."));
			}
			Package pkg = builder.getPackage();
			// log debug info
			if (log.isDebugEnabled()) {
				log.debug(String.format("[Drools package compiler]: the '%1$s' binary package has been created.", pkg.getName()));
			}
			
        	// return compiled package
        	return pkg;
		} catch (Exception e) {
			if (e instanceof RuntimeDroolsException) {
				// log already contains the information about exception - just re-throw it
				throw (RuntimeDroolsException)e;
			} else {
				// need to log exception before throwing it
				log.error(String.format("[Drools package compiler]: error compiling the source files: %1$s", drlFiles), e);
				throw new RuntimeDroolsException(e);
			}
		} finally {
			// just in case
    		IOUtils.closeQuietly (inputStream); 
		}
    }

    /**
     * Creates rule base from business rule sources (single package) 
     * 
     * @param  ruleFlow the rule flow for drl files or null
     * @param drlFiles source files. Supported file names as strings and as URLs
     * @return rule base or throws Runtime Exception
     * @throws RuntimeDroolsException
     */
    public static RuleBase createRuleBase  (Object ruleFlow, Object ... drlFiles) {
    	// compile
    	Package pkg = compilePackage (ruleFlow, drlFiles);
		// return
		return createRuleBase (pkg);
    }

    /**
     * Creates rule base from business rule sources (single package) 
     * 
     * @param drlFiles source files. Supported file names as strings and as URLs
     * @return rule base or throws Runtime Exception
     * @throws RuntimeDroolsException
     */
    public static RuleBase createRuleBase  (Object ... drlFiles) {
    	// compile
    	Package pkg = compilePackage (drlFiles);
		// return
		return createRuleBase (pkg);
    }
    
    /**
     * Creates rule base and adds compiled packages 
     * 
     * @param packages the compiled packages to add.  
     * @return rule base or throws Runtime Exception
     * @throws RuntimeDroolsException
     */
    public static RuleBase createRuleBase  (Package ... packages) {
		// return
		return addPackages (null, packages);
    }

    /**
     * Creates rule base if needed and adds compiled packages 
     * @param rule base (may be null)
     * @param packages the compiled packages to add.  
     * @return rule base or throws Runtime Exception
     * @throws RuntimeDroolsException
     */
    public static RuleBase addPackages (RuleBase ruleBase, Package ... packages) {
    	RuleBase rb = ruleBase != null? ruleBase : RuleBaseFactory.newRuleBase(); 
		try {
			for (Package pkg : packages) {
    			// log debug info
    			if (log.isDebugEnabled()) {
    				log.debug(String.format("Adding the '%1$s' package to the rule base (%2$s)...", pkg.getName(), rb.toString()));
    			}
				
				// add package to a rulebase (deploy the rule package).
				rb.addPackage( pkg );
			}
		} catch (Exception e) {
			// need to log exception before throwing it
			log.error(String.format("Drools rule base compilation error..."), e);
			// throw
			throw (e instanceof RuntimeDroolsException)? (RuntimeDroolsException)e : new RuntimeDroolsException(e);  
		}
		// return
		return rb;
    }

    /**
     * Runs business rules using rule base to create new working memory
     * 
     * @param ruleBase the rule base to use in order to create working memory
     * @param retractFacts if true then retract facts after run
     * @param facts the facts to introduce to working memory
     * @return business rules working memory
     */
    public static WorkingMemory executeRules (RuleBase ruleBase, boolean retractFacts, Object... facts) {
    	// check rule 
    	if (ruleBase == null) {
    		throw new RuntimeDroolsException ("RuleBase shall be defined");
    	}
    	
    	// return 
    	return executeRules (ruleBase, null, retractFacts, facts);
    }
    
    /**
     * Runs business rules using rule base to create new working memory
     * 
     * @param ruleBase the rule base to use in order to create working memory
     * @param agendaGroup the agenda group to focus
     * @param retractFacts if true then retract facts after run
     * @param facts the facts to introduce to working memory
     * @return business rules working memory
     */
    public static WorkingMemory executeRules (RuleBase ruleBase, String agendaGroup, boolean retractFacts, Object... facts) {
    	// check rule 
    	if (ruleBase == null) {
    		throw new RuntimeDroolsException ("RuleBase shall be defined");
    	}
    	
    	// return 
    	return executeRules (ruleBase.newStatefulSession(), agendaGroup, retractFacts, facts);
    }

    /**
     * Runs business rules using provided working memory
     * 
     * @param workingMemory the working memory to use
     * @param retractFacts if true then retract facts after run
     * @param facts the facts to introduce to working memory
     * @return business rules working memory
     */
    public static WorkingMemory executeRules (WorkingMemory workingMemory, boolean retractFacts, Object... facts) {
    	// return 
    	return executeRules (workingMemory, null, retractFacts, facts);
    }
    
    /**
     * Runs business rules using provided working memory
     * 
     * @param workingMemory the working memory to use
     * @param agendaGroup the agenda group to focus
     * @param retractFacts if true then retract facts after run
     * @param facts the facts to introduce to working memory
     * @return business rules working memory
     */
    public static WorkingMemory executeRules (WorkingMemory workingMemory, String agendaGroup, boolean retractFacts, Object... facts) {
    	// check input 
    	if (workingMemory == null) {
    		throw new RuntimeDroolsException ("Business rules working memory shall be defined");
    	}
		// log debug info
		if (log.isDebugEnabled()) {
			log.debug(String.format("[Rules Runner]: preparing to execute business rules for the '%1$s' rule base.", workingMemory.getRuleBase().toString()));
		}
    	
    	// storage for handles
    	List<FactHandle> factHandles = new ArrayList<FactHandle>();
    	//
    	// set focus on agenda group if provided
    	//
    	if (agendaGroup != null) {
			// log debug info
			if (log.isDebugEnabled()) {
				log.debug(String.format("[Rules Runner]: The focus set to the '%1$s' agenda.", agendaGroup));
			}
			// set focus 
    		workingMemory.setFocus(agendaGroup);
    	}
    	// insert facts
        for (Object fact : facts) {
			// log debug info
			if (log.isDebugEnabled()) {
				log.debug(String.format("[Rules Runner]: Inserting the '%1$s' fact into the working memory...", fact));
			}
			// introduce fact
			FactHandle fh = workingMemory.insert( fact );
			if (retractFacts) {
				factHandles.add(fh);
			}
        }
        //
        // fire rules
        //
		// log debug info
		if (log.isDebugEnabled()) {
			log.debug(String.format("[Rules Runner]: executing business rules for the '%1$s' rule base...", workingMemory.getRuleBase().toString()));
		}
        workingMemory.fireAllRules();
        
		// log debug info
		if (log.isDebugEnabled()) {
			log.debug(String.format("[Rules Runner]: business rules executed for the '%1$s' rule base.", workingMemory.getRuleBase().toString()));
		}
		
        // retract if needed
        if (retractFacts) {
        	for (FactHandle fh : factHandles) {
    			// log debug info
    			if (log.isDebugEnabled()) {
    				log.debug(String.format("[Rules Runner]: Retracting the '%1$s' fact handle from the working memory...", fh.toExternalForm()));
    			}
        		workingMemory.retract(fh);
        	}
        }
        // return
        return workingMemory;
    }
}
